/*
 * Copyright © 2017 eqxiu.com 北京中网易企秀科技有限公司  All rights reserved.
 */

package cn.hermit.dubbo.docs.scanner;

import cn.hermit.data.json.model.JSONArray;
import cn.hermit.data.json.model.JSONDocument;
import cn.hermit.data.json.model.JSONFactory;
import cn.hermit.data.json.model.JSONObject;
import cn.hermit.data.swagger.SwaggerContext;
import cn.hermit.nio.http.Http;
import cn.hermit.nio.http.server.HttpHandler;
import cn.hermit.nio.http.server.rest.RestfulHandler;
import cn.hermit.nio.http.server.rest.annotation.Operation;
import cn.hermit.nio.http.server.rest.annotation.Param;
import cn.hermit.nio.http.server.rest.annotation.Resource;
import cn.hermit.nio.http.server.rest.annotation.Response;
import cn.hermit.nio.http.server.rest.annotation.Tag;
import cn.hermit.util.ReflectionUtils;
import cn.hermit.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentMap;

/**
 * @author JackGao 高建国
 * @description //TODO 设计说明
 * @date 4/6/2017
 */
public class HermitScanner {

    public void scan(SwaggerContext swaggerContext) throws ClassNotFoundException {
        ConcurrentMap<String, HttpHandler> handlerCaches = (ConcurrentMap<String, HttpHandler>) swaggerContext.get("httpHandlerCaches");
        String basePath = (String) swaggerContext.get("basePath");
        Object api = swaggerContext.get("apiObj");
        Class<?> clz = (Class<?>) swaggerContext.get("interface");
        JSONObject swagger = swaggerContext.getSwagger();
        JSONFactory jsonFactory = swaggerContext.getJsonFactory();
        Resource resource = clz.getAnnotation(Resource.class);
        JSONArray tagsArr = (JSONArray) swagger.getValue("tags");
        Tag[] tags = resource.tags();
        for (Tag tag : tags) {
            JSONObject tagObj = jsonFactory.createObject();
            jsonFactory.objectEntry(tagObj, "name", tag.name());
            jsonFactory.objectEntry(tagObj, "description", tag.description());
            jsonFactory.arrayValue(tagsArr, tagObj);
        }
        JSONObject pathsObj = (JSONObject) swagger.getValue("paths");
        Method[] methods = clz.getDeclaredMethods();
        for (Method method : methods) {

            String path = basePath + '/' + method.getName();
            handlerCaches.putIfAbsent(path, new RestfulHandler(method, api));

            JSONObject pathObj = jsonFactory.createObject();
            jsonFactory.objectEntry(pathsObj, path, pathObj);
            JSONObject methodObj = jsonFactory.createObject();
            jsonFactory.objectEntry(pathObj, Http.Verb.POST.name().toLowerCase(), methodObj);
            JSONArray tagsStrArr = jsonFactory.createArray();
            jsonFactory.objectEntry(methodObj, "tags", tagsStrArr);

            Operation operation = method.getAnnotation(Operation.class);
            String operationId = null;
            String[] tagStrArr = null;
            String summary = null;
            String description = null;
            String[] produces = null;
            JSONObject responsesObj = null;
            if (operation != null) {
                operationId = operation.operationId();
                tagStrArr = operation.tags();
                summary = operation.summary();
                description = operation.description();
                produces = operation.produces();
                Response[] responses = operation.responses();
                if (responses != null && responses.length > 0) {
                    responsesObj = jsonFactory.createObject();
                    jsonFactory.objectEntry(methodObj, "responses", responsesObj);
                    for (Response response : responses) {
                        JSONObject responseObj = jsonFactory.createObject();
                        jsonFactory.objectEntry(responsesObj, String.valueOf(response.code()), responseObj);
                        jsonFactory.objectEntry(responseObj, "description", response.description());
                        String schema = response.schema();
                        if (!StringUtils.isBlank(schema)) {
                            JSONObject schemaObj = jsonFactory.createObject();
                            jsonFactory.objectEntry(responseObj, "schema", schemaObj);
                            if (schema.startsWith("[")) {
                                jsonFactory.objectEntry(schemaObj, "type", "array");
                                jsonFactory.objectEntry(schemaObj, "$ref", "#/definitions/" + schema.substring(1, schema.length() - 1));
                            } else {
                                jsonFactory.objectEntry(schemaObj, "$ref", "#/definitions/" + schema);
                            }
                        }
                    }
                }
            }

            if (StringUtils.isBlank(operationId)) {
                operationId = clz.getSimpleName() + '_' + method.getName();
            }

            jsonFactory.objectEntry(methodObj, "operationId", operationId);

            if (tagStrArr == null || tagStrArr.length == 0) {
                tagStrArr = new String[tagsArr.size()];
                Iterator<JSONDocument> tagsIterator = tagsArr.valueIterator();
                int i = 0;
                while (tagsIterator.hasNext()) {
                    JSONObject tagObj = (JSONObject) tagsIterator.next();
                    tagStrArr[i++] = tagObj.getValue("name").toString();
                }
            }

            JSONArray tagArr = jsonFactory.createArray();
            jsonFactory.objectEntry(methodObj, "tags", tagArr);
            for (String tagStr : tagStrArr) {
                jsonFactory.arrayValue(tagArr, tagStr);
            }

            if (StringUtils.isBlank(summary)) {
                if (StringUtils.isBlank(description)) {
                    summary = description = clz.getName() + '.' + method.getName();
                } else {
                    summary = description;
                }
            } else {
                if (StringUtils.isBlank(description)) {
                    description = summary;
                }
            }

            jsonFactory.objectEntry(methodObj, "summary", summary);
            jsonFactory.objectEntry(methodObj, "description", description);

            Class<?> returnType = method.getReturnType();
            if (produces == null || produces.length == 0) {
                if (ReflectionUtils.isBaseType(returnType)) {
                    produces = new String[]{Http.MEDIA_TYPE_TEXT_PLAIN};
                } else {
                    produces = new String[]{Http.MEDIA_TYPE_APPLICATION_JSON};
                }
            }

            JSONArray producesArr = jsonFactory.createArray();
            jsonFactory.objectEntry(methodObj, "produces", producesArr);

            jsonFactory.arrayValue(producesArr, produces[0]);

            if (!ReflectionUtils.isBaseType(returnType)) {
                boolean isArray = false;
                if (returnType.isArray()) {
                    returnType = returnType.getComponentType();
                    isArray = true;
                } else if (Collection.class.isAssignableFrom(returnType)) {
                    Type t = method.getGenericReturnType();
                    List<Type> typeList = ReflectionUtils.getParameterizedTypes(t);
                    if (typeList.size() > 0) {
                        returnType = ReflectionUtils.getClass(typeList.get(0));
                    }
                    isArray = true;
                }

                String schemeName = returnType.getSimpleName();
                swaggerContext.put("schemaName", schemeName);
                swaggerContext.put("schemaType", returnType);
                checkSchemaDefinition(swaggerContext);

                if (responsesObj == null) {
                    responsesObj = jsonFactory.createObject();
                    jsonFactory.objectEntry(methodObj, "responses", responsesObj);
                }

                JSONObject okResponseObj = (JSONObject) responsesObj.getValue("200");
                if (okResponseObj == null) {
                    okResponseObj = jsonFactory.createObject();
                    jsonFactory.objectEntry(responsesObj, "200", okResponseObj);
                    jsonFactory.objectEntry(okResponseObj, "description", clz.getName() + '.' + method.getName() + ".response");
                }
                JSONObject schemaObj = (JSONObject) okResponseObj.getValue("schema");
                if (schemaObj == null) {
                    schemaObj = jsonFactory.createObject();
                    jsonFactory.objectEntry(okResponseObj, "schema", schemaObj);
                    if (isArray) {
                        jsonFactory.objectEntry(schemaObj, "type", "array");
                        JSONObject itemsObj = jsonFactory.createObject();
                        jsonFactory.objectEntry(schemaObj, "items", itemsObj);
                        jsonFactory.objectEntry(itemsObj, "$ref", "#/definitions/" + schemeName);
                    } else {
                        jsonFactory.objectEntry(schemaObj, "$ref", "#/definitions/" + schemeName);
                    }
                }

            }

            Class<?>[] parameterTypes = method.getParameterTypes();
            Annotation[][] parameterAnnotations = method.getParameterAnnotations();
            if (parameterTypes == null || parameterTypes.length == 0) {
                continue;
            } else {
                JSONArray parametersArr = jsonFactory.createArray();
                jsonFactory.objectEntry(methodObj, "parameters", parametersArr);
                String[] paramNames = ReflectionUtils.getParameterNames(method);
                for (int i = 0; i < parameterTypes.length; i++) {
                    JSONObject parameterObj = jsonFactory.createObject();
                    jsonFactory.arrayValue(parametersArr, parameterObj);
                    Class<?> parameterType = parameterTypes[i];
                    String name = null;
                    Http.ParamIn in = null;
                    boolean required = false;
                    String _default = null;
                    String paramDesc = null;
                    Annotation[] annotations = parameterAnnotations[i];
                    if (annotations == null || annotations.length == 0) {
                        name = paramNames[i];
                        paramDesc = clz.getName() + '.' + method.getName() + '.' + name;
                    } else {
                        Annotation annotation = annotations[0];
                        if (annotation.annotationType() == Param.class) {
                            //TODO
                            Param param = (Param) annotation;
                            name = param.name();
                            in = param.in();
                            required = param.required();
                            _default = param._default();
                            paramDesc = param.description();
                        } else {
                            name = paramNames[i];
                            paramDesc = clz.getName() + '.' + method.getName() + '.' + name;
                        }
                    }
                    jsonFactory.objectEntry(parameterObj, "name", name);
                    jsonFactory.objectEntry(parameterObj, "description", paramDesc);
                    if (ReflectionUtils.isBaseType(parameterType) || String.class == parameterType) {
                        swaggerContext.put("parameterObj", parameterObj);
                        swaggerContext.put("parameterType", parameterType);
                        handleParamType(swaggerContext);
                    } else {
                        if (parameterTypes.length == 1) {
                            jsonFactory.objectEntry(parameterObj, "in", Http.ParamIn.BODY.getValue());
                            jsonFactory.objectEntry(parameterObj, "name", "body");
                            String schemeName = parameterType.getSimpleName();
                            swaggerContext.put("schemaName", schemeName);
                            swaggerContext.put("schemaType", parameterType);
                            checkSchemaDefinition(swaggerContext);
                            JSONObject schemaObj = jsonFactory.createObject();
                            jsonFactory.objectEntry(parameterObj, "schema", schemaObj);
                            jsonFactory.objectEntry(schemaObj, "$ref", "#/definitions/" + schemeName);

                            JSONArray consumesArr = jsonFactory.createArray();
                            jsonFactory.objectEntry(methodObj, "consumers", consumesArr);
                            jsonFactory.arrayValue(consumesArr, Http.MEDIA_TYPE_APPLICATION_JSON);
                        } else {
                            //TODO
                        }
                    }
                    if (!StringUtils.isBlank(paramDesc)) {
                        jsonFactory.objectEntry(parameterObj, "description", paramDesc);
                    }
                    if (!StringUtils.isBlank(_default)) {
                        jsonFactory.objectEntry(parameterObj, "default", _default);
                    }
                    if (required) {
                        jsonFactory.objectEntry(parameterObj, "required", required);
                    }

                }
            }

        }
    }

    private void checkSchemaDefinition(SwaggerContext swaggerContext) {
        String schemaName = (String) swaggerContext.get("schemaName");
        Class<?> schemaType = (Class<?>) swaggerContext.get("schemaType");
        JSONObject swagger = swaggerContext.getSwagger();
        JSONFactory jsonFactory = swaggerContext.getJsonFactory();
        JSONObject definitionsObj = (JSONObject) swagger.getValue("definitions");
        JSONObject definitionObj = (JSONObject) definitionsObj.getValue(schemaName);
        if (definitionObj == null) {
            definitionObj = jsonFactory.createObject();
            jsonFactory.objectEntry(definitionsObj, schemaName, definitionObj);
            jsonFactory.objectEntry(definitionObj, "type", "object");
            JSONObject propertiesObj = jsonFactory.createObject();
            jsonFactory.objectEntry(definitionObj, "properties", propertiesObj);
            Field[] fields = schemaType.getDeclaredFields();
            if (fields != null || fields.length > 0) {
                //TODO
                for (Field field : fields) {
                    int modifiers = field.getModifiers();
                    if (Modifier.isStatic(modifiers) || !Modifier.isPrivate(modifiers)) {
                        continue;
                    }
                    Class<?> fieldType = field.getType();
                    boolean isArray = false;
                    if (fieldType.isArray()) {
                        fieldType = fieldType.getComponentType();
                        isArray = true;
                    } else if (Collection.class.isAssignableFrom(fieldType)) {
                        Type type = field.getGenericType();
                        if (ParameterizedType.class.isInstance(type)) {
                            ParameterizedType pType = (ParameterizedType) type;
                            Type[] pts = pType.getActualTypeArguments();
                            int length = pts.length;
                            Class<?>[] clzs = new Class<?>[length];
                            for (int i = 0; i < length; i++) {
                                clzs[i] = (Class<?>) pts[i];
                            }
                            fieldType = clzs[0];
                        } else {
                            fieldType = String.class;
                        }
                        isArray = true;
                    }

                    Http.ParamRealType paramRealType = Http.ParamRealType.getRealType(fieldType);
                    if (paramRealType != null) {
                        String dataType = Http.ParamRealType.getSwaggerType(paramRealType);
                        String dataFormat = Http.ParamRealType.getDataFormat(paramRealType);
                        JSONObject paramObj = jsonFactory.createObject();
                        if (isArray) {
                            jsonFactory.objectEntry(paramObj, "type", "array");
                            JSONObject itemsObj = jsonFactory.createObject();
                            jsonFactory.objectEntry(paramObj, "items", itemsObj);
                            jsonFactory.objectEntry(itemsObj, "type", dataType);
                            jsonFactory.objectEntry(itemsObj, "format", dataFormat);
                            JSONObject xmlObj = jsonFactory.createObject();
                            jsonFactory.objectEntry(paramObj, "xml", xmlObj);
                            //TODO
                            jsonFactory.objectEntry(xmlObj, "name", field.getName());
                            jsonFactory.objectEntry(xmlObj, "wrapped", true);
                        } else {
                            jsonFactory.objectEntry(paramObj, "type", dataType);
                            jsonFactory.objectEntry(paramObj, "format", dataFormat);
                        }

                        jsonFactory.objectEntry(propertiesObj, field.getName(), paramObj);

                    } else {
                        //TODO
                    }
                }
            }
            JSONObject xmlObj = jsonFactory.createObject();
            jsonFactory.objectEntry(definitionObj, "xml", xmlObj);
            jsonFactory.objectEntry(xmlObj, "name", schemaName);
        }
    }

    private void handleParamType(SwaggerContext swaggerContext) {
        JSONObject paramObj = (JSONObject) swaggerContext.get("parameterObj");
        Class<?> parameterType = (Class<?>) swaggerContext.get("parameterType");
        JSONFactory jsonFactory = swaggerContext.getJsonFactory();
        boolean isArray = false;
        if (parameterType.isArray()) {
            parameterType = parameterType.getComponentType();
            isArray = true;
        } else if (Collection.class.isAssignableFrom(parameterType)) {
            parameterType = String.class;
            isArray = true;
        }
        parameterType = ReflectionUtils.getPrimitiveType(parameterType);
        if (parameterType != null) {
            Http.ParamRealType paramRealType = Http.ParamRealType.getRealType(parameterType);
            String dataType = Http.ParamRealType.getSwaggerType(paramRealType);
            String dataFormat = Http.ParamRealType.getDataFormat(paramRealType);
            jsonFactory.objectEntry(paramObj, "in", Http.ParamIn.FORM.getValue());
            if (isArray) {
                jsonFactory.objectEntry(paramObj, "type", "array");
                JSONObject itemsObj = jsonFactory.createObject();
                jsonFactory.objectEntry(paramObj, "items", itemsObj);
                jsonFactory.objectEntry(itemsObj, "type", dataType);
                jsonFactory.objectEntry(itemsObj, "format", dataFormat);
            } else {
                jsonFactory.objectEntry(paramObj, "type", dataType);
                jsonFactory.objectEntry(paramObj, "format", dataFormat);
            }
        }
    }

}
